(function($) {
    var bindings = [],
        templates = [],
        parser = (function() {
        var contentHandler = new DefaultHandler2(),
            bindingObject,
            elements = (function() {
                var types = {},
                    getTypes = function() {
                        return types;
                    };

                contentHandler.attributeDecl = function(eName, aName, type, mode, value) {
                    types[eName] = 'object';
                };

                contentHandler.elementDecl = function(name, model) {
                    switch (model) {
                        case '(#PCDATA)':
                            types[name] = 'field';
                            break;

                        case 'EMPTY':
                            types[name] = 'empty';
                            break;

                        default:
                            types[name] = 'object';
                    }
                };

                return {
                    getTypes: getTypes
                };
            }()),
            unmarshalling = function(xml) {
                var saxParser = XMLReaderFactory.createXMLReader(),
                    stack = [{}];

                contentHandler.startElement = function(namespaceURI, localName, qName, atts) {
                    var obj = {},
                        i;

                    switch (elements.getTypes()[localName]) {
                        case 'empty':
                        case 'field':
                            stack.push(localName);
                            break;
                        case 'object':
                            for (i = 0; i < atts.getLength(); i += 1) {
                                obj['__' + atts.getLocalName(i)] = atts.getValue(i);
                            }
                            stack.push(obj);
                            break;
                        default:
                            throw "DTD Error";
                    }
                };

                contentHandler.endElement = function(namespaceURI, localName, qName) {
                    var item = stack.pop(),
                        parent,
                        type = elements.getTypes()[localName];

                    if (type == 'empty') {
                        item = undefined;
                    }

                    if (type == 'object' || type == 'empty') {
                        parent = stack[stack.length - 1];

                        if (parent.hasOwnProperty(localName)) {
                            if (!Array.isArray(parent[localName])) {
                                parent[localName] = [parent[localName]];
                            }
                            parent[localName].push(item);
                        } else {
                            parent[localName] = item;
                        }
                    }
                };

                contentHandler.characters = function(ch, start, length) {
                    var item = stack.pop(),
                        obj;

                    if (typeof item == "string") {
                        obj = stack[stack.length - 1];

                        if (obj.hasOwnProperty(item)) {
                            if (!Array.isArray(obj[item])) {
                                obj[item] = [obj[item]];
                            }
                            obj[item].push(ch);
                        } else {
                            obj[item] = ch;
                        }
                    } else {
                        item['__content'] = ch;
                    }

                    stack.push(item);
                };

                contentHandler.endDocument = function() {
                    bindingObject = stack[0];
                };

                if (xml) {
                    saxParser.setHandler(contentHandler);
                    saxParser.parseString(xml);
                } else {
                    throw "XML Parser Error";
                }
            },
            marshalling = function() {
                var xmlStr2 = "<?xml version=\"1.0\"?>",
                    xmlStr = "",
                    process = function(obj, elem) {
                        var name;
                        
                        for (field in obj) {
                            name = elem || field;

                            if (typeof obj[field] == 'string') {
                                if (field.substr(0, 2) == '__') {
                                    if (field == '__content') {
                                        xmlStr += obj[field];
                                    } else {
                                        attr = field.replace('__', '') +'="'+ obj[field] +'"';
                                        xmlStr = xmlStr.substr(0, xmlStr.length - 1) +" "+ attr + ">";
                                    }
                                } else {
                                    xmlStr += "<"+ name +">" + obj[field] + "</"+ name +">";
                                }
                            } else if (Array.isArray(obj[field])) {
                                process(obj[field], field);
                            } else if (typeof obj[field] == 'undefined') {
                                xmlStr += "<"+ name +"/>";
                            } else {
                                xmlStr += "<"+ name +">";
                                process(obj[field]);
                                xmlStr += "</"+ name +">";
                            }
                        }
                    };

                process(bindingObject);
                return xmlStr;
            },
            getObject = function() {
                return bindingObject;
            };

        return {
            unmarshalling: unmarshalling,
            marshalling: marshalling,
            getObject: getObject
        };
    }());

    $.fn.XMLBinder = function(options) {
        var object,
            standardEvents = 'change DOMCharacterDataModified',
            options = $.extend({
                tag: '#',
                standardEvents: true,
                insertTag: true
            }, options),
            updateObjectField = function($elem, obj, field, index) {
                var value,
                    type = isNaN(index) ? typeof obj[field] : typeof obj[field][index];

                if (type == 'string') {
                    value = $elem.is(':text') ? $elem.val() : $elem.text();
                }

                if (isNaN(index)) {
                    obj[field] = value;
                } else {
                    obj[field][index] = value;
                }
            },
            updateXML = function(domElement) {
                options.xml = parser.marshalling();
                $(bindings).trigger('updateXML', { triggerElement: domElement });
            };
            insertTag = function($elem) {
                if ($elem.is(':text') && $.trim($elem.val()) == '') {
                    $elem.val(options.tag);
                } else if ($.trim($elem.text()) == '') {
                    $elem.text(options.tag);
                }
            },
            insertValue = function($elem, value) {
                if ($elem.is(':text')) {
                    $elem.val($elem.val().replace(options.tag, value));
                } else {
                    $elem.text($elem.text().replace(options.tag, value));
                }

                return $elem;
            },
            createElement = function($elem) {
                return $elem.clone();
            },
            createElements = function($base, num) {
                var i,
                    $elem = $base,
                    $elems = $();

                for (i = 0; i < num; i += 1) {
                    $elem = createElement($base).insertAfter($elem);
                    $elems = $elems.add($elem);
                }
                
                return $elems;
            },
            saveTemplate = function($elem) {
                var index = templates.length;

                $elem.attr('data-template', '#' + index);
                templates.push('#');
            },
            deleteEvents = function($elem) {
                var template = $elem.attr('data-template'),
                    hasTemplate = '[data-template="'+ template +'"]',
                    events = options.standardEvents ? standardEvents : undefined;

                $elem.unbind(events).siblings(hasTemplate).unbind(events);
            },
            restoreTemplate = function($elem) {
                var template = $elem.attr('data-template'),
                    hasTemplate = '[data-template="'+ template +'"]',
                    index = template.substr(1);

                if ($elem.is(':text')) {
                    $elem.val(templates[index]);
                } else {
                    $elem.text(templates[index]);
                }
            },
            fillTemplate = function($elem, value) {
                var i,
                    val,
                    result = [],
                    $elems = [];

                if (!Array.isArray(value)) value = [value];

                $elem.each(function(i) {
                    if (typeof value[i] == 'string') {
                        val = value[i];  
                    }
                    result.push(insertValue($(this), val)[0]);
                });

                return result;
            },
            bindStandardEvents = function($elems, obj, field, domBinding) {
                $elems.bind(standardEvents, function(e) {
                    var $elem = $(this),
                        index = undefined;

                    if (Array.isArray(obj[field]) && $elem.index() <= obj[field].length) {
                        index = $elem.index();
                    }

                    updateObjectField($elem, obj, field, index);
                    updateXML(domBinding);
                });
            },
            getTargetElements = function($base, obj, field) {
                var $elems = $base,
                    $siblings = $base.siblings('.' + field);

                if ($siblings.length == 0) {
                    return $elems.add(createElements($base, obj[field].length - 1));
                }

                return $elems.add($siblings);
            },
            performBinding = function($elem, obj, field, rebuild, domBinding) {
                var elems;

                if (rebuild) {
                    deleteEvents($elem);
                    restoreTemplate($elem);   
                } else {
                    saveTemplate($elem);
                }

                if (options.insertTag) {
                    insertTag($elem);
                }

                elems = fillTemplate($elem, obj[field]);

                if (options.standardEvents) {
                    bindStandardEvents($(elems), obj, field, domBinding);
                }
            },
            start = function(obj, $elem, rebuild, domBinding) {
                var field, $elems;

                for (field in obj) {
                    if ($elem.hasClass(field) || $elem.attr('id') == field) {
                        if (Array.isArray(obj[field])) {
                            $elems = getTargetElements($elem, obj, field);

                            if (typeof obj[field][0] == 'object') {
                                $elems.each(function(i) {
                                    $(this).children().each(function(j) {
                                       start(obj[field][i], $(this), rebuild, domBinding);
                                    });
                                }); 
                            } else {
                                performBinding($elems, obj, field, rebuild, domBinding);
                            }
                        } else if (typeof obj[field] == 'object') {
                            $elem.children().each(function() {
                               start(obj[field], $(this), rebuild, domBinding);
                            });
                        } else {
                            performBinding($elem, obj, field, rebuild, domBinding);
                        }
                    }
                }
            };

        if (options.dtd && options.xml) {
            parser.unmarshalling(options.dtd + options.xml);
            object = parser.getObject();

            this.each(function() {
                bindings.push(this);
                start(object, $(this), false, this);

                $(this).bind('updateXML', function(e, extra) {
                    if (this != extra.triggerElement) {
                        start(object, $(this), true, this);
                    }
                });
            });

            return {
                getXML: function() {
                    return options.xml;
                },
                getBindingObject: function() {
                    return object;
                }
            };
        }
    };
})(jQuery);
